import 'package:meta/meta.dart';
import 'package:yeild_libs_dal/src/context.dart';
import 'package:yeild_libs_dal/src/repository.dart';

/// Callback for repository changes.
typedef DalRepoChangedCallback = Future<void> Function(DalRepository? repo);

/// Data Access Layer (DAL) scope.
/// A DAL scope is a context that can be used to manage repositories
/// and provide data access functionality within a specific scope.
abstract class DalScope extends DalContext {
  /// The parent scope of this DAL scope.
  final DalScope? parent;

  /// A map of child scopes of this DAL scope.
  final Map<Type, DalScope> children = {};

  /// A map of repositories registered in this DAL scope.
  final Map<Type, DalRepository> repositories = {};

  /// A map of callbacks for repository changes.
  /// The key is the repository type, and the value is the callback function.
  final Map<Type, List<DalRepoChangedCallback>> repoChangedCallbacks = {};

  DalScope([this.parent]);

  /// Initializes the DAL scope.
  /// This method should be called before using the DAL scope.
  /// It initializes all the repositories that are registered in the scope.
  /// This method returns true if all repositories are initialized successfully,
  /// otherwise it returns false.
  @override
  Future<bool> init() async {
    if (!await super.init()) return false;
    var result = await Future.wait(
      children.values.map((child) => child.init()),
    );
    if (result.any((test) => test == false)) return false;
    result = await Future.wait(repositories.values.map((repo) => repo.init()));
    if (result.any((test) => test == false)) return false;
    return true;
  }

  /// Resets the DAL scope.
  /// @param [cascade] If true, it will reset all child contexts as well.
  @override
  Future<bool> reset([bool cascade = true]) async {
    super.reset(cascade);
    if (!cascade) return true;
    var result = await Future.wait(
      children.values.map((child) => child.reset(cascade)),
    );
    if (result.any((test) => test == false)) return false;
    result = await Future.wait(
      repositories.values.map((repo) => repo.reset(cascade)),
    );
    return !result.any((test) => test == false);
  }

  /// Dispose the DAL scope.
  @override
  void dispose() {
    for (var child in children.values.toList(growable: false)) {
      child.dispose();
    }
    for (var repo in repositories.values.toList(growable: false)) {
      repo.dispose();
    }
    parent?.unregisterScope(this);
    children.clear();
    repositories.clear();
    repoChangedCallbacks.clear();
    super.dispose();
  }

  /// Creates the DAL scope.
  /// This method should be called when the DAL scope is created.
  @protected
  @override
  void onCreate() {
    super.onCreate();
    registerSelf();
  }

  /// Registers the DAL scope to its parent.
  @protected
  void registerSelf() {
    parent?.registerScope(this);
  }

  /// Initializes the DAL scope.
  /// This method should be called after the DAL scope is created.
  /// It is important to initialize resources to prevent memory leaks.
  @protected
  @override
  Future<bool> onInit() async {
    return await super.onInit();
  }

  /// Cleans up resources when the DAL scope is reseted.
  @protected
  @override
  Future<bool> onReset() async {
    parent?.unregisterScope(this);
    return await super.onReset();
  }

  /// Cleans up resources when the DAL scope is destroied.
  @protected
  @override
  void onDisposed() {
    for (var child in children.values.toList(growable: false)) {
      child.dispose();
    }
    for (var repo in repositories.values.toList(growable: false)) {
      repo.dispose();
    }
    parent?.unregisterScope(this);
    super.onDisposed();
  }

  /// Adds a child scope to the current DAL scope.
  /// If the child scope is already registered, it throws an exception.
  void registerScope<Scope extends DalScope>(Scope child) {
    if (children.containsKey(Scope)) {
      throw Exception('Child scope $Scope already registered in parent scope.');
    }
    children[Scope] = child;
  }

  /// Unregisters a child scope from the current DAL scope.
  /// If the child scope is not registered, it throws an exception.
  void unregisterScope<Scope extends DalScope>(Scope child) {
    if (children.containsKey(Scope)) children.remove(Scope);
    if (children.containsValue(child)) {
      children.removeWhere((key, value) => identical(value, child));
    }
  }

  /// Retrieves a child scope from the DAL scope.
  /// If the child scope is not registered, it returns null.
  Scope? scope<Scope extends DalScope>() {
    if (this is Scope) return this as Scope;
    return (children[Scope] ?? parent?.scope<Scope>()) as Scope?;
  }

  /// Registers a repository in the scope.
  /// If the repository is already registered, it throws an exception.
  void registerRepo<Repo extends DalRepository>(Repo pRepo) {
    if (repositories.containsKey(Repo)) {
      throw Exception('Repository $Repo already registered in scope.');
    }
    repositories[Repo] = pRepo;
    repoChangedCallbacks[Repo]?.forEach((callback) => callback(repo()));
  }

  /// Unregisters a repository from the scope.
  void unregisterRepo<Repo extends DalRepository>(Repo pRepo) {
    if (repositories.containsKey(Repo)) repositories.remove(Repo);
    if (repositories.containsValue(pRepo)) {
      repositories.removeWhere((key, value) => identical(value, pRepo));
    }
    repoChangedCallbacks[Repo]?.forEach((callback) => callback(repo()));
  }

  /// Retrieves a repository from the DAL scope.
  Repo? repo<Repo extends DalRepository>() {
    return (repositories[Repo] ?? parent?.repo<Repo>()) as Repo?;
  }

  /// Watches for changes in a repository.
  /// If the repository is already being watched, returns false, otherwise returns true.
  bool watchRepo<Repo extends DalRepository>(DalRepoChangedCallback callback) {
    var callbacks = repoChangedCallbacks.putIfAbsent(
      Repo,
      () => <DalRepoChangedCallback>[],
    );
    if (callbacks.contains(callback)) return false;
    callbacks.add(callback);
    callback(repo<Repo>());
    return true;
  }

  /// Unwatches a repository for changes.
  void unwatchRepo<Repo extends DalRepository>(
    DalRepoChangedCallback callback,
  ) {
    unwatchRepoByType(callback, Repo);
  }

  /// Unwatches a repository for changes by [type].
  void unwatchRepoByType(DalRepoChangedCallback callback, Type type) {
    if (repoChangedCallbacks.containsKey(type)) {
      repoChangedCallbacks[type]?.remove(callback);
      if (repoChangedCallbacks[type]?.isEmpty == true) {
        repoChangedCallbacks.remove(type);
      }
    }
  }
}
